package cn.tedu.mall.seckill.service.impl;

import cn.tedu.mall.common.exception.CoolSharkServiceException;
import cn.tedu.mall.common.restful.JsonPage;
import cn.tedu.mall.common.restful.ResponseCode;
import cn.tedu.mall.pojo.product.vo.SpuDetailStandardVO;
import cn.tedu.mall.pojo.product.vo.SpuStandardVO;
import cn.tedu.mall.pojo.seckill.model.SeckillSpu;
import cn.tedu.mall.pojo.seckill.vo.SeckillSpuDetailSimpleVO;
import cn.tedu.mall.pojo.seckill.vo.SeckillSpuVO;
import cn.tedu.mall.product.service.seckill.IForSeckillSpuService;
import cn.tedu.mall.seckill.mapper.SeckillSpuMapper;
import cn.tedu.mall.seckill.service.ISeckillSpuService;
import cn.tedu.mall.seckill.utils.RedisBloomUtils;
import cn.tedu.mall.seckill.utils.SeckillCacheUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.math.RandomUtils;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class SeckillSpuServiceImpl implements ISeckillSpuService {

    // 装配查询全部秒杀商品信息的mapper
    @Autowired
    private SeckillSpuMapper seckillSpuMapper;
    // 需要根据spuId查询常规spu信息,要dubbo调用product模块
    @DubboReference
    private IForSeckillSpuService dubboSeckillSpuService;

    // 分页查询秒杀商品信息
    // 返回值泛型为SeckillSpuVO,这个类型定义了spu常规信息和spu秒杀信息的属性
    // 我们需要从两方面为SeckillSpuVO对象赋值
    @Override
    public JsonPage<SeckillSpuVO> listSeckillSpus(Integer page, Integer pageSize) {
        // 设置分页查询条件
        PageHelper.startPage(page,pageSize);
        // 执行查询秒杀商品信息的方法
        List<SeckillSpu> seckillSpus=seckillSpuMapper.findSeckillSpus();
        // 实例化一个SeckillSpuVO泛型的集合,用于最终返回
        List<SeckillSpuVO> seckillSpuVOs=new ArrayList<>();
        // 遍历从秒杀数据库表中查询出的所有商品信息集合:seckillSpus
        for(SeckillSpu spu : seckillSpus){
            // 声明取出spuId以备后面使用
            Long spuId=spu.getSpuId();
            // 根据spuId查询spu常规信息
            SpuStandardVO standardVO = dubboSeckillSpuService.getSpuById(spuId);
            // 实例化SeckillSpuVO对象,以便接收秒杀信息和常规信息
            SeckillSpuVO spuVO=new SeckillSpuVO();
            // 将standardVO同名属性赋值到spuVO对象中
            BeanUtils.copyProperties(standardVO,spuVO);
            // 常规信息上面赋值完毕了,下面要赋值秒杀信息
            spuVO.setSeckillListPrice(spu.getListPrice());
            spuVO.setStartTime(spu.getStartTime());
            spuVO.setEndTime(spu.getEndTime());
            // 到此为止,我们spuVO对象的常规信息和秒杀信息就都被赋值了!
            seckillSpuVOs.add(spuVO);
        }
        // 最后返回的是JsonPage类型的对象,需要进行转换
        return JsonPage.restPage(new PageInfo<>(seckillSpuVOs));
    }

    // 装配操作Redis的对象
    @Autowired
    private RedisTemplate redisTemplate;

    // 装配操作布隆过滤器的对象
    @Autowired
    private RedisBloomUtils redisBloomUtils;
    // SeckillSpuVO返回值,是既包含常规信息,又包含秒杀信息的spu对象
    @Override
    public SeckillSpuVO getSeckillSpu(Long spuId) {
        // 这个方法一开始就要进行布隆过滤器的判断
        // 使用布隆过滤器判断参数spuId是否在数据库中,如果不在直接抛出异常
        // 先获取布隆过滤器的key
        String bloomDayKey=
                SeckillCacheUtils.getBloomFilterKey(LocalDate.now());
        // 判断这个key是否存在
        if(! redisTemplate.hasKey(bloomDayKey)){
            throw new CoolSharkServiceException(
                    ResponseCode.INTERNAL_SERVER_ERROR,
                    "布隆过滤器未加载");
        }
        // 判断参数spuId是否在布隆过滤器中,如果不存在直接抛出异常
        if( ! redisBloomUtils.bfexists(bloomDayKey,spuId+"")){
            throw new CoolSharkServiceException(
                    ResponseCode.NOT_FOUND,
                    "您要访问的商品不存在(布隆过滤器生效)");
        }
        // 下面要先判断Redis中是否已经保存了这个SeckillSpuVO对象
        // 先确定Redis的key
        // mall:seckill:spu:vo:2
        String spuVOKey= SeckillCacheUtils.getSeckillSpuVOKey(spuId);
        // 声明一个返回值类型的对象,方便后续操作
        SeckillSpuVO seckillSpuVO=null;
        // 判断Redis中是否已经有这个Key
        if(redisTemplate.hasKey(spuVOKey)){
            // 如果Redis中有这个key,直接从Redis中获取
            seckillSpuVO= (SeckillSpuVO)
                    redisTemplate.boundValueOps(spuVOKey).get();
        }else{
            // 如果Redis中没有这个key,就要从数据库查询了
            // 要查询秒杀信息和常规信息,最后都赋值到seckillSpuVO对象
            SeckillSpu seckillSpu=seckillSpuMapper.findSeckillSpuById(spuId);
            // 判断一下这个seckillSpu对象是否为null(因为布隆过滤器会有误判)
            if(seckillSpu == null){
                // 如果是null,抛出异常(发生缓存穿透)
                throw new CoolSharkServiceException(
                        ResponseCode.NOT_FOUND,"您要访问的商品不存在");
            }
            // 查询spu常规信息
            SpuStandardVO spuStandardVO =
                    dubboSeckillSpuService.getSpuById(spuId);
            // 将常规信息和秒杀信息都保存到seckillSpuVO对象中
            // 先实例化seckillSpuVO对象
            seckillSpuVO=new SeckillSpuVO();
            BeanUtils.copyProperties(spuStandardVO,seckillSpuVO);
            // 赋值秒杀信息
            seckillSpuVO.setSeckillListPrice(seckillSpu.getListPrice());
            seckillSpuVO.setStartTime(seckillSpu.getStartTime());
            seckillSpuVO.setEndTime(seckillSpu.getEndTime());
            // 将seckillSpuVO对象保存到Redis中,方便后面的请求直接从Redis中获取
            redisTemplate.boundValueOps(spuVOKey).set(
                    seckillSpuVO,
                    1000*60*5+ RandomUtils.nextInt(10000),
                    TimeUnit.MILLISECONDS);
        }
        // 到此为止,seckillSpuVO对象包含了常规信息和秒杀信息,但是url属性没有赋值
        // url属性的作用就是给前端用于提交秒杀订单的
        // 可以这么理解,一旦url有值,就相当于允许用户秒杀购买此商品了
        // 下面我们要判断当前时间是否在这个商品允许购买的世界范围内
        // 获取当前时间
        LocalDateTime nowTime=LocalDateTime.now();
        // 判断nowTime是否在商品的秒杀开始时间和结束时间之间
        // 根据尽量不连数据库的原则,我们使用java代码判断一下
        // 判断逻辑是   开始时间早于当前时间 && 当前时间早于结束时间
        if (seckillSpuVO.getStartTime().isBefore(nowTime)
            && nowTime.isBefore(seckillSpuVO.getEndTime())){
            // 进入这个if表示时间合适,可以为url赋值
            // 要从Redis中获取随机码,作为url的一部分(随机码已经预热)
            String randCodeKey=SeckillCacheUtils.getRandCodeKey(spuId);
            if( ! redisTemplate.hasKey(randCodeKey)){
                // 注意判断前面有 ! 当没有这个key时,才会进if
                throw new CoolSharkServiceException(
                        ResponseCode.NOT_FOUND,"随机码不存在(等下一分钟再试)");
            }
            // key正常存在,获取随机码,赋值到url中
            int randCode= (int) redisTemplate.boundValueOps(randCodeKey).get();
            seckillSpuVO.setUrl("/seckill/"+randCode);
            log.info("商品详细信息构建完成,url属性为{}",seckillSpuVO.getUrl());
        }
        // 最后别忘了返回seckillSpuVO!!!!!!!!!
        return seckillSpuVO;
    }

    // 项目定义的常量类中没有给定SpuDetail使用的值,所以我们自己定义一个
    public static final String SECKILL_SPU_DETAIL_PREFIX="seckill:spu:detail:";

    // 根据spuId查询秒杀用spuDetail信息
    @Override
    public SeckillSpuDetailSimpleVO getSeckillSpuDetail(Long spuId) {
        // 先获取spuDetail对应的key
        String spuDetailKey=SECKILL_SPU_DETAIL_PREFIX+spuId;
        SeckillSpuDetailSimpleVO simpleVO=null;
        // 判断这个key是否存在
        if(redisTemplate.hasKey(spuDetailKey)){
            // 如果存在,直接从Redis中获取
            simpleVO= (SeckillSpuDetailSimpleVO)
                    redisTemplate.boundValueOps(spuDetailKey).get();
        }else{
            // 如果不存在,需要从数据库查询
            // dubbo调用product模块查询spuDetail对象
            SpuDetailStandardVO spuDetailStandardVO=
                    dubboSeckillSpuService.getSpuDetailById(spuId);
            // 先实例化simpleVO
            simpleVO=new SeckillSpuDetailSimpleVO();
            BeanUtils.copyProperties(spuDetailStandardVO,simpleVO);
            // 保存到Redis中
            redisTemplate.boundValueOps(spuDetailKey).set(
                    simpleVO,
                    1000*60*4+RandomUtils.nextInt(10000),
                    TimeUnit.MILLISECONDS);
        }
        // 最后别忘了返回!!!!
        return simpleVO;
    }




}
